<?php
/**
* Class implementing command-line interface. Supports GNU and Unix style
* options.
* 
* @package Framework
* @subpackage Core
* @author Gustav Calder <gustav.calder@gmail.com> 
*/
namespace TwinPeaks\Core;
require_once 'CApplication.php';

abstract class CCommandLine extends CApplication
{
	const OPTION_STYLE_GNU = 1;
	const OPTION_STYLE_UNIX = 2;
	const OPTION_STYLE_UNKNOWN = 3;
	const OPTION_FLAG_REQUIRED = 1;
	
	/**
	* @var integer Number of passed arguments minus one (script filename).
	*/
	private $argsCount = 0;
	
	/**
	* @var
	*/
	private $lastParsedOptionId;
	
	/**
	* @var mixed
	*/
	private $lastParsedOptionStyle;
	
	/**
	* @var array Options container
	*/
	private $optTable = [];
	
	/**
	* @return void
	*/
	abstract protected function defineOptions();
	
	/**
	 * @return void
	 */
	function defineNodes() {/* ... */}
		
	
	/**
	* Terminates script execution.
	* 
	* @final
	* @access public
	* @param string $errMessage Error message
	* @return void
	*/
	final static function shutdown($errMessage)
	{
		$args = func_get_args();
		echo call_user_func_array('sprintf', $args);
		echo "\n";
		exit(1);
	}
	/**
	* 
	*/
	final protected function userError() { }
	/**
	* Returns option id by its long name (GNU option name without hyphens).
	* 
	* @param string $name Option name.
	* @return integer
	*/
	private function getOptionIdByLongName($name)
	{
		foreach ($this->optTable as $id => $entry)
		{
			if ($entry['long-name'] == $name) {
				return $id;
			}
		}
	return false;
	}
	
	/**
	* @see CShell::getOptionIdByLongName
	*/
	private function getOptionIdByShortName($name)
	{
		foreach ($this->optTable as $id => $entry)
		{
			if ($entry['short-name'] == $name)
				return $id;
		}
		return false;
	}
	
	/**
	* @return mixed
	*/
	private function getLastParsedOptionId()
	{
		return $this->lastParsedOptionId;
	}
	
	/**
	* @return integer
	*/
	private function getLastParsedOptionStyle()
	{
		return $this->lastParsedOptionStyle;
	}
	
	/**
	 * Adds an option definition.
	 * 
	 * @param inrteger $optId Opt id.
	 * @param string $shortName Short option name, Unix style.
	 * @param string $longName Long option name, GNU style.
	 * @param mixed $defaultValue Option default value in the case it was not specified.
	 * A callback function is allowed.
	 * @param integer $optFlags Bitfield, option flags.
	 * @return void
	 */
	final protected function addOption($optId, $shortName, $longName = null, $defaultValue = null, $optFlags = 0)
	{
		$this->optTable[$optId] = [
			'short-name' => $shortName,
			'long-name' => $longName,
			'default' => $defaultValue,
			'value' => null,
			'flags' => $optFlags
		];
	}
	
	/**
	 * @return boolean
	 */
	final static function isGnuOption($sample, &$crumbs)
	{
		$matches = null;
		$result = preg_match('|^--([^=]+)(=([\"\'])?([^\"\']*)\3?)?$|', $sample, $matches);
		if ($result)
		{
			$crumbs = [];
			$crumbs['name'] = $matches[1];
			$crumbs['value'] = count($matches) < 3 ? null : // Option has no value.
			$matches[4]; // With value.
		}
		return $result;
	}
	
	/**
	 * @return boolean
	 */
	final static function isUnixOption($sample, &$crumbs)
	{
		$matches = null;
		$result = preg_match('|^-([a-zA-Z0-9])(.*)?$|', $sample, $matches);
		if ($result)
		{
			$crumbs = [];
			$crumbs['name'] = $matches[1];
			$crumbs['value'] = isset($matches[2]) ? $matches[2] : null;
		}
		return $result;
	}
	
	/**
	 * Returns option default value.
	 * 
	 * @param integer $optId Option id.
	 * @return mixed
	 */
	final function getDefaultOptionValue($optId)
	{
		$value = $this->optTable[$optId]['default'];
		if (is_callable($value))
			return $value();
		else
			return $value;
	}
	
	/**
	 * Returns option value.
	 * 
	 * @param integer $optId Option id.
	 * @return mixed
	 */
	final function getOptionValue($optId)
	{
		return $this->optTable[$optId]['value'];
	}
	
	/**
	 * Sets up option value.
	 * 
	 * @access protected
	 * @final
	 * @param integer $optId Option id.
	 * @param mixed $value Option value.
	 * @return void
	 */
	final protected function setOptionValue($optId, $value)
	{
		$this->optTable[$optId]['value'] = $value;
	}
	
	/**
	 * 
	 * @final
	 * @access public
	 * @return integer Option style id.
	 */
	private function parseOption($option)
	{
		$crumbs = null;
		$optId = false;
		$style = self::OPTION_STYLE_UNKNOWN;
		if (self::isGnuOption($option, $crumbs))
		{
			$optId = $this->getOptionIdByLongName($crumbs['name']);
			$style = self::OPTION_STYLE_GNU;
		}
		else if (self::isUnixOption($option, $crumbs))
		{
			$optId = $this->getOptionIdByShortName($crumbs['name']);
			$style = self::OPTION_STYLE_UNIX;
		}
		if ($optId === false)
		{
			if ($style != self::OPTION_STYLE_UNKNOWN)
			{
				self::shutdown("\n-- Wrong option '%s' has been specified. Try --help for help.\n",
				$crumbs['name']);
		}
		return $style;
		}
		$this->lastParsedOptionId = $optId;
		$this->lastParsedOptionStyle = $style;
		$optValue = is_null($crumbs['value']) ?
		$this->getDefaultOptionValue($optId) :
		$crumbs['value'];
		$this->setOptionValue($optId, $optValue);
		return $style;
	}
	
	/**
	 * Returns true if option was set up.
	 *
	 * @param integer $optId Option id.
	 * @return boolean
	*/
	final protected function hasOption($optId)
	{
		return isset($this->optTable[$optId]);
	}
	
	/**
	 * Returns true if option value can be converted to boolean TRUE.
	 * 
	 * @return boolean
	 */
	final protected function optionHasTrueValue($optId)
	{
		return !empty($this->optTable[$optId]['value']);
	}
	
	/**
	 * @return
	 */
	final protected function parseArgs($argv)
	{
		$this->argsCount = count($argv);
		$this->defineOptions();
		foreach ($argv as $value)
		{
			$optionStyle = $this->parseOption($value);
			if ($optionStyle == self::OPTION_STYLE_UNKNOWN &&
			$this->getLastParsedOptionStyle() != self::OPTION_STYLE_UNKNOWN)
			{
				$this->setOptionValue($this->getLastParsedOptionId(), $value);
			}
		}
	}
	
	/**
	 * @return array
	 */
	protected function scriptGetArgs()
	{
		return $_SERVER['argv'];
	}
	
	/**
	 * 
	 * @return void
	 */
	final protected function onCreate()
	{
		$args = func_get_args();
		if (count($args))
			$this->parseArgs($args[0]);
		else
		{
			$realArgs = $this->scriptGetArgs();
			$filename = array_shift($realArgs);
			$this->scriptFilename = realpath(getcwd() . '/' . $filename);
			$this->parseArgs($realArgs);
		}
		
		parent::onCreate();
	}
	
	/**
	 * @return CCommandLine
	 */
	final static function run($args = null)
	{
		$scriptInstance = static::getInstance($args);
		
		return $scriptInstance;
	}
}
